package com.gxar.file.server.base.sdk;

import com.alibaba.fastjson.JSON;
import com.aliyun.oss.ClientConfiguration;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.*;
import com.gxar.fusion.redis.service.RedisService;
import com.google.common.collect.Lists;
import com.gxar.common.exception.APIRuntimeException;
import com.gxar.file.server.base.common.constant.FileConstant;
import com.gxar.file.server.base.common.enums.ProductEnum;
import com.gxar.file.server.base.common.enums.TargetChannelEnum;
import com.gxar.file.server.base.model.ResultCodeEnum;
import com.gxar.file.server.base.model.upload.request.FilePartUploadRequest;
import com.gxar.file.server.base.model.upload.response.FilePartUploadResponse;
import com.gxar.file.server.db.model.ProductEntity;
import com.gxar.file.server.db.respository.ProductRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * aliyun 文件上传管理
 */
@Slf4j
@Component("aliYunOssHelper")
@RequiredArgsConstructor
public class AliYunOssHelper {

    private final static Integer MAX_CONNECTIONS = 1024;
    private final static Integer SOCKET_TIMEOUT = 90000;
    private final static Integer MAX_ERROR_RETRY = 2;
    private final static Map<String, OSSClient> OSS_CLIENT_MAP = new ConcurrentHashMap<>();
    private final static Map<String, ProductEntity> PRODUCT_MAP = new ConcurrentHashMap<>();

    private final ProductRepository productRepository;

    private final RedisService redisRepository;

    private synchronized OSSClient getOSSClient(String productName) {
        OSSClient ossClient = OSS_CLIENT_MAP.get(productName);
        if (ossClient != null) {
            return ossClient;
        } else {
            ProductEntity productEntity = productRepository.findByNameAndChannel(productName, TargetChannelEnum.OSS.getCode());
            if (Objects.nonNull(productEntity)) {
                PRODUCT_MAP.put(productName, productEntity);
                ossClient = new OSSClient(productEntity.getEndpoint(), productEntity.getAccessKey(),
                        productEntity.getAccessSecret(), getClientConfig());
                OSS_CLIENT_MAP.put(productName, ossClient);
                return ossClient;
            } else {
                return null;
            }
        }
    }

    private static ClientConfiguration getClientConfig() {
        // 创建ClientConfiguration实例
        ClientConfiguration conf = new ClientConfiguration();
        // 设置OSSClient使用的最大连接数，默认1024
        conf.setMaxConnections(MAX_CONNECTIONS);
        // 设置请求超时时间，默认50秒
        conf.setSocketTimeout(SOCKET_TIMEOUT);
        // 设置失败请求重试次数，默认2次
        conf.setMaxErrorRetry(MAX_ERROR_RETRY);
        return conf;
    }

    /**
     * 上传文件，并且设置为公共可读文件
     *
     * @param productEnum 产品code
     * @param fileName    文件名
     * @param streamFile  流
     * @param publicRead  是否公共读
     * @return 是否上传成功
     */
    public Boolean uploadAndSetAclFile(ProductEnum productEnum, String fileName, InputStream streamFile, Boolean publicRead) {
        try {
            OSSClient ossClient = getOSSClient(productEnum.getName());
            if (Objects.isNull(ossClient)) {
                log.error("[{}]该产品没有OSS客户端", productEnum.getName());
                return false;
            }
            boolean exists = ossClient.doesBucketExist(PRODUCT_MAP.get(productEnum.getName()).getBucketName());
            if (!exists) {
                log.error("bucket不存在");
                return false;
            }
            // 上传文件。
            ossClient.putObject(PRODUCT_MAP.get(productEnum.getName()).getBucketName(), fileName, streamFile);
            if (publicRead != null && publicRead) {
                ossClient.setObjectAcl(PRODUCT_MAP.get(productEnum.getName()).getBucketName(), fileName, CannedAccessControlList.PublicRead);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("[{}]文件上传失败:{}", fileName, e.getMessage(), e);
        } finally {
            try {
                if (streamFile != null) {
                    streamFile.close();
                }
            } catch (IOException e) {
                log.error("[{}]文件流关闭失败:{}", fileName, e.getMessage(), e);
            }
        }
        return false;
    }

    /**
     * 获取到上传oss的图片的url
     *
     * @param product  产品
     * @param fileName 文件名称
     * @return url
     */
    public String getOssImgUrl(ProductEnum product, String fileName) {
        String imgUrl = "";
        if (StringUtils.isNotBlank(fileName)) {
            try {
                String oriFileName = fileName.substring(fileName.lastIndexOf("/") + 1);
                oriFileName = URLEncoder.encode(oriFileName, "UTF-8");
                fileName = fileName.substring(0, fileName.lastIndexOf("/") + 1) + oriFileName;
            } catch (UnsupportedEncodingException e) {
                log.debug(e.getMessage());
            }
            ProductEntity productEntity = PRODUCT_MAP.get(product.getName());
            if (Objects.nonNull(productEntity)) {
                imgUrl = productEntity.getEndpoint().substring(productEntity.getEndpoint().indexOf("://") + 3);
                imgUrl = "https://" + productEntity.getBucketName() + "." + imgUrl + "/" + fileName;
            }
        }
        return imgUrl;
    }

    /**
     * 分片上传
     * 1、检查文件是否上传
     * 2、检查文件是否第一次上传，第一次上传创建上传id uploadId
     * 3、检查是否是断点续传，如果是返回已上传的分片
     * 4、分片上传到阿里云OSS上，并记录上传信息到Redis
     * 5、判断是否已上传完成，已完成：合并所有分片为源文件
     *
     * @param productEnum 产品
     * @param file        文件分片
     * @param request     文件分片上传请求
     * @return 上传结果
     */
    public FilePartUploadResponse uploadChunk(ProductEnum productEnum, String fileName, FilePartUploadRequest request, MultipartFile file) {
        if (StringUtils.isEmpty(fileName)) {
            throw new APIRuntimeException(ResultCodeEnum.UPLOAD_FILE_NAME_ERROR);
        }
        OSSClient ossClient = getOSSClient(productEnum.getName());
        if (Objects.isNull(ossClient)) {
            throw new APIRuntimeException(ResultCodeEnum.UNKNOWN_OSS_CLIENT);
        }
        try {
            // 判断是否上传
            if (fileExists(productEnum, fileName)) {
                FilePartUploadResponse response = new FilePartUploadResponse();
                response.setSkipUpload(true);
                response.setUrl(getOssImgUrl(productEnum, fileName));
                return response;
            }
            // 判断是否第一次上传
            if (StringUtils.isBlank(request.getUploadId())) {
                request.setUploadId(uploadChunkInit(productEnum, fileName));
                FilePartUploadResponse response = new FilePartUploadResponse();
                response.setSkipUpload(false);
                response.setUploadId(request.getUploadId());
                response.setUploaded(null);
                return response;
            }

            // 检查分片是否已上传 实现断点续传
            if (file == null) {
                Map<Object, Object> uploadedCache = redisRepository.hmGetAll(FileConstant.REDIS_ALI_OSS_KEY + request.getUploadId());
                List<Integer> uploaded = Lists.newArrayList();
                for (Map.Entry<Object, Object> entry : uploadedCache.entrySet()) {
                    uploaded.add(JSON.parseObject(entry.getValue().toString(), PartETag.class).getPartNumber());
                }
                FilePartUploadResponse response = new FilePartUploadResponse();
                response.setSkipUpload(false);
                response.setUploadId(request.getUploadId());
                response.setUploaded(uploaded);
                return response;
            }
            // 上传分片
            PartETag partETag = uploadPartFile(productEnum, fileName, request.getUploadId(),
                    file.getInputStream(), request.getChunkSize(), request.getChunkNumber());
            if (Objects.isNull(partETag)) {
                log.error("分片上传失败 上传响应为空");
                throw new APIRuntimeException(ResultCodeEnum.UPLOAD_TAG_FAIL);
            }
            // 分片上传完成缓存key
            redisRepository.hmSet(FileConstant.REDIS_ALI_OSS_KEY + request.getUploadId(), request.getChunkNumber() + ",", JSON.toJSONString(partETag));
            // 取出所有已上传的分片信息
            Map<Object, Object> dataMap = redisRepository.hmGetAll(FileConstant.REDIS_ALI_OSS_KEY + request.getUploadId());
            List<PartETag> partETagList = Lists.newArrayList();
            for (Map.Entry<Object, Object> entry : dataMap.entrySet()) {
                partETagList.add(JSON.parseObject(entry.getValue().toString(), PartETag.class));
            }
            FilePartUploadResponse response = new FilePartUploadResponse();
            // 判断是否上传完成
            if (dataMap.keySet().size() == request.getTotalChunks()) {
                uploadChunkComplete(productEnum, request.getUploadId(), fileName, partETagList, request.getPublics());
                for (Object mapKey : dataMap.keySet()) {
                    redisRepository.hdel(FileConstant.REDIS_ALI_OSS_KEY + request.getUploadId(), mapKey.toString());
                }
                ObjectMetadata objectMetadata = getObjectMetadata(ossClient, PRODUCT_MAP.get(productEnum.getName()).getBucketName(), fileName);
                response.setSkipUpload(true);
                response.setUploadId(request.getUploadId());
                response.setUrl(getOssImgUrl(productEnum, fileName));
                response.setFinish(true);
                response.setSize(objectMetadata.getContentLength());
            } else {
                List<Integer> list = partETagList.stream().map(PartETag::getPartNumber).collect(Collectors.toList());
                response.setUploaded(list);
                response.setSkipUpload(false);
                response.setUploadId(request.getUploadId());
            }
            return response;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("上传失败，请检查文件正确性", e);
            throw new APIRuntimeException(ResultCodeEnum.UPLOAD_FAIL);
        }
    }

    /**
     * 初始化上传id
     *
     * @param productEnum 产品
     * @param key         文件名
     * @return uploadId
     */
    public String uploadChunkInit(ProductEnum productEnum, String key) {
        if (StringUtils.isEmpty(key) || Objects.isNull(productEnum)) {
            throw new APIRuntimeException(ResultCodeEnum.KEY_NOT_NULL);
        }
        OSSClient ossClient = getOSSClient(productEnum.getName());
        if (Objects.isNull(ossClient)) {
            throw new APIRuntimeException(ResultCodeEnum.UNKNOWN_OSS_CLIENT);
        }
        try {
            // 创建分片上传对象
            InitiateMultipartUploadRequest uploadRequest = new InitiateMultipartUploadRequest(PRODUCT_MAP.get(productEnum.getName()).getBucketName(), key);
            // 初始化分片
            InitiateMultipartUploadResult result = ossClient.initiateMultipartUpload(uploadRequest);
            // 返回uploadId，它是分片上传事件的唯一标识，您可以根据这个uploadId发起相关的操作，如取消分片上传、查询分片上传等。
            return result.getUploadId();
        } catch (Exception e) {
            e.printStackTrace();
            throw new APIRuntimeException(ResultCodeEnum.CHUNK_INIT_FAIL);
        }
    }

    /**
     * 上传分片文件
     *
     * @param productEnum 产品
     * @param key         文件名
     * @param uploadId    上传id
     * @param inputStream 文件
     * @param chunkSize   分片大小
     * @param chunkIndex  分片索引
     * @return 分片tag
     */
    public PartETag uploadPartFile(ProductEnum productEnum, String key, String uploadId, InputStream inputStream,
                                   Long chunkSize, Integer chunkIndex) {
        OSSClient ossClient = getOSSClient(productEnum.getName());
        if (Objects.isNull(ossClient)) {
            throw new APIRuntimeException(ResultCodeEnum.UNKNOWN_OSS_CLIENT);
        }
        try {
            ProductEntity productEntity = PRODUCT_MAP.get(productEnum.getName());
            UploadPartRequest partRequest = new UploadPartRequest();
            partRequest.setBucketName(productEntity.getBucketName());
            partRequest.setKey(key);
            // 分片上传uploadId
            partRequest.setUploadId(uploadId);
            // 分片文件
            partRequest.setInputStream(inputStream);
            // 分片大小。除了最后一个分片没有大小限制，其他的分片最小为100 KB。
            partRequest.setPartSize(chunkSize);
            // 分片号。每一个上传的分片都有一个分片号，取值范围是1~10000，如果超出这个范围，OSS将返回InvalidArgument的错误码。
            partRequest.setPartNumber(chunkIndex);
            // 每个分片不需要按顺序上传，甚至可以在不同客户端上传，OSS会按照分片号排序组成完整的文件。
            UploadPartResult uploadPartResult = ossClient.uploadPart(partRequest);
            return uploadPartResult.getPartETag();

        } catch (Exception e) {
            e.printStackTrace();
        } catch (Throwable e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    log.error("[{}]文件流关闭失败:{}", key, e.getMessage(), e);
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    /**
     * 分片上传文件合并
     *
     * @param productEnum 产品
     * @param uploadId    上传id
     * @param key         key
     * @param chunkTags   分片上传信息
     * @param publicRead  是否公共读
     * @return 响应
     */
    public CompleteMultipartUploadResult uploadChunkComplete(ProductEnum productEnum, String uploadId, String key, List<PartETag> chunkTags, Boolean publicRead) {
        OSSClient ossClient = getOSSClient(productEnum.getName());
        if (Objects.isNull(ossClient)) {
            throw new APIRuntimeException(ResultCodeEnum.UNKNOWN_OSS_CLIENT);
        }
        try {
            CompleteMultipartUploadRequest completeMultipartUploadRequest =
                    new CompleteMultipartUploadRequest(PRODUCT_MAP.get(productEnum.getName()).getBucketName(), key, uploadId, chunkTags);
            CompleteMultipartUploadResult result = ossClient.completeMultipartUpload(completeMultipartUploadRequest);
            if (publicRead != null && publicRead) {
                ossClient.setObjectAcl(PRODUCT_MAP.get(productEnum.getName()).getBucketName(), key, CannedAccessControlList.PublicRead);
            }
            return result;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("分片合成失败 {}", e.getMessage(), e);
            throw new APIRuntimeException(ResultCodeEnum.CHUNK_COMPLETE_FAIL);
        }
    }

    /**
     * 普通下载文件到指定路径
     *
     * @param fileName     下载的文件名
     * @param downloadFile 完整的指定路径，需要指定到文件名称
     */
    public boolean downloadFile(ProductEnum productEnum, String fileName, String downloadFile) {
        try {
            OSSClient ossClient = getOSSClient(productEnum.getName());
            if (Objects.isNull(ossClient)) {
                return false;
            }
            ProductEntity productEntity = PRODUCT_MAP.get(productEnum.getName());
            // 下载OSS文件到本地文件。如果指定的本地文件存在会覆盖，不存在则新建。
            ossClient.getObject(new GetObjectRequest(productEntity.getBucketName(), fileName), new File(downloadFile));
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 删除文件
     *
     * @param fileName 文件名
     */
    public void deleteFile(String productName, String fileName) {
        // 删除Object。
        OSSClient ossClient = getOSSClient(productName);
        if (Objects.isNull(ossClient)) {
            return;
        }
        ProductEntity productEntity = PRODUCT_MAP.get(productName);
        ossClient.deleteObject(productEntity.getBucketName(), fileName);
    }

    /**
     * 判断文件是否存在
     *
     * @param fileName 文件名
     * @return 存在
     */
    public boolean fileExists(ProductEnum productEnum, String fileName) {
        OSSClient ossClient = getOSSClient(productEnum.getName());
        if (Objects.isNull(ossClient)) {
            return false;
        }
        ProductEntity productEntity = PRODUCT_MAP.get(productEnum.getName());
        // Object是否存在。
        return ossClient.doesObjectExist(productEntity.getBucketName(), fileName);
    }

    /**
     * 存储空间是否存在
     *
     * @return 存在
     */
    public boolean bucketExist(ProductEnum productEnum) {
        OSSClient ossClient = getOSSClient(productEnum.getName());
        if (Objects.isNull(ossClient)) {
            return false;
        }
        ProductEntity productEntity = PRODUCT_MAP.get(productEnum.getName());
        return ossClient.doesBucketExist(productEntity.getBucketName());
    }

    /**
     * 将文件copy到本地进行中转
     *
     * @param file 文件
     * @return 返回存储路径
     */
    public String copyFileToLocal(MultipartFile file) {
        String path = System.getProperty("user.dir");
        String name = file.getOriginalFilename();
        String fileName = "name";
        if (name != null) {
            fileName = name.substring(0, name.lastIndexOf(".")) + System.currentTimeMillis() + name.substring(name.lastIndexOf("."));
        }
        File saveFile = new File(path, fileName);
        try {
            file.transferTo(saveFile);
            return path + File.separator + fileName;
        } catch (IllegalStateException | IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 下载流文件
     *
     * @param fileName 文件名称
     * @return 流
     */
    public InputStream downloadStreamFile(ProductEnum productEnum, String fileName) {
        OSSClient ossClient = getOSSClient(productEnum.getName());
        if (Objects.isNull(ossClient)) {
            return null;
        }
        ProductEntity productEntity = PRODUCT_MAP.get(productEnum.getName());
        OSSObject object = ossClient.getObject(productEntity.getBucketName(), fileName);
        return object.getObjectContent();
    }

    /**
     * 下载文件到响应流
     *
     * @param productEnum 产品
     * @param fileKey     文件名称
     * @param response    响应
     * @return 下载错误信息
     */
    public String downloadFile(ProductEnum productEnum, String fileKey, HttpServletResponse response) {
        OSSClient ossClient = getOSSClient(productEnum.getName());
        if (Objects.isNull(ossClient)) {
            return "该产品暂未开通，敬请期待";
        }
        ProductEntity productEntity = PRODUCT_MAP.get(productEnum.getName());
        ServletOutputStream data = null;
        InputStream is = null;
        String msg = "";
        try {
            String bucketName = productEntity.getBucketName();
            // ossObject包含文件所在的存储空间名称、文件名称、文件元信息以及一个输入流。
            OSSObject ossObject = ossClient.getObject(bucketName, fileKey);

            // 读取文件内容。
            is = ossObject.getObjectContent();
            data = response.getOutputStream();
            // 5M一次读写
            byte[] buffer = new byte[1024 * 1024 * 5];
            int len;
            while ((len = is.read(buffer)) != -1) {
                data.write(buffer, 0, len);
            }
        } catch (OSSException oe) {
            log.error("下载文件客户端异常[{}]：{}", fileKey, oe.getMessage(), oe);
            msg = "下载文件客户端异常";
        } catch (Throwable ce) {
            log.error("下载文件未知异常[{}]：{}", fileKey, ce.getMessage(), ce);
            msg = "下载文件未知异常";
        } finally {
            if (null != is) {
                try {
                    is.close();
                } catch (IOException e) {
                    log.error("下载文件流异常[{}]:{}", fileKey, e.getMessage(), e);
                    msg = "下载文件流异常";
                }
            }
            if (null != data) {
                try {
                    data.close();
                } catch (IOException e) {
                    log.error("下载文件响应流异常[{}]:{}", fileKey, e.getMessage(), e);
                    msg = "下载文件响应流异常：" + fileKey;
                }
            }
        }
        return msg;
    }

    /**
     * 获取对象元数据
     *
     * @param ossClient  客户端
     * @param bucketName bucket
     * @param fileName   文件名
     * @return 元数据
     */
    public ObjectMetadata getObjectMetadata(OSSClient ossClient, String bucketName, String fileName) {
        return ossClient.getObjectMetadata(bucketName, fileName);
    }
}
